#!/bin/bash
#
# clandestine.googlecode.com
#

#$Clandestine shell library %LIBCLANDESTINE%

##This software uses Clandestine shell library. This library will parse
##  command line and will react automatically for the following GNU options:
##
##  --debug             Enable the debug mode
##  --help              Print this help and exit
##  --version           Show version information and exit

# Okay, let's begin
#
# SHELL OPTIONS
#
shopt -qs expand_aliases # enable aliases
set -eu # be strict
set +H # don't use history expansion
#
# CONSTANTS
#
readonly -a LIBCLANDESTINE=( 0 1 2 "rocket fuel" )
readonly COLR=$'\e[m' # "normal" colour
readonly COLF_BLACK=$'\e[0;30m'
readonly COLF_RED=$'\e[0;31m'
readonly COLF_GREEN=$'\e[0;32m'
readonly COLF_BROWN=$'\e[0;33m'
readonly COLF_BLUE=$'\e[0;34m'
readonly COLF_MAGENTA=$'\e[0;35m'
readonly COLF_CYAN=$'\e[0;36m'
readonly COLF_GREY=$'\e[0;37m'
readonly COLF_LIGHTGREY=$'\e[1;30m'
readonly COLF_LIGHTRED=$'\e[1;31m'
readonly COLF_LIGHTGREEN=$'\e[1;32m'
readonly COLF_YELLOW=$'\e[1;33m'
readonly COLF_LIGHTBLUE=$'\e[1;34m'
readonly COLF_PINK=$'\e[1;35m'
readonly COLF_AZURE=$'\e[1;36m'
readonly COLF_WHITE=$'\e[1;37m'

readonly RE_INT='^([+-]?0*[1-6]?[[:digit:]]#)?[+-]?[[:digit:]]+$'
#
# ALIASES
#
alias not="!"
alias String="declare --"
alias Integer="declare -i"
alias Array="declare -a"
#
# ERROR REPORTING
#
function Error () {
	# FIXME trace pretty-print, caller
	String   FNAME="$1"   LINE="$2"   LEVEL="$3"
	Array   TRACE=( "${FUNCNAME[@]:1}" )
	[[ "$LEVEL" = "DEBUG" ]] && IsEmpty GNUOPT_debug && return 0 # do not write debugs while not in debug mode
	shift 3
	echo "[file=$FNAME:$LINE pid=$$ uid=$UID pwd=$PWD timer=$SECONDS trace=($TRACE)] $LEVEL: $*." > /dev/stderr
	[[ "$LEVEL" = "FATAL" ]] && exit 1
	return 0
}
alias Debug='Error "$BASH_SOURCE" $LINENO DEBUG'
alias  Warn='Error "$BASH_SOURCE" $LINENO WARNING'
alias Fatal='Error "$BASH_SOURCE" $LINENO FATAL'
#
# FUNCTION FUNCTIONS
#
function Usage () {
	Debug "Usage: ${FUNCNAME[1]} $*"
	return 1
}
function FunctionLookup () {
	String FUNCTIONS=`declare -F | sed 's/^declare -f //g'`
	(( $# < 1 )) && { echo "$FUNCTIONS"; return 0; }
	grep -q -e "$1" <<< "$FUNCTIONS"
}
function FunctionCallProcess () {
	#(( $# < 1 ))
	String   FORMAT="$1"   V=""   DECLARES=""
#	for V in $FORMAT; do
		
#	done
	echo "$DECLARES"
}
#
# VARIABLE FUNCTIONS
#
function TypeOf () {
	(( $# < 1 )) && Usage "<variable>"
	String   V=""   TYPE=""
	for V in `eval echo "\\${!$1*}"`; do # iterate through variable names that begin with $1
		if [[ "$V" = "$1" ]]; then # found an exact match
			read TYPE TYPE V <<< "`declare -p "$V"`"
			echo ${TYPE:1} # 'a' for an array, 'i' for an integer, '-' for a string
			return 0
		fi
	done
	return 1 # not found
}
function IsSet () {
	(( $# < 1 )) && Usage "<variable>"
	TypeOf $1 > /dev/null
}
function IsString () {
	(( $# < 1 )) && Usage "<variable>"
	[[ "-" = "`TypeOf "$1"`" ]]
}
function IsInteger () {
	(( $# < 1 )) && Usage "<variable>"
	[[ "i" = "`TypeOf "$1")`" ]]
}
function IsArray () {
	(( $# < 1 )) && Usage "<variable>"
	[[ "a" = "`TypeOf "$1"`" ]]
}
function Length () {
	(( $# < 1 )) && Usage "<variable>"
	String   TYPE=""
	TYPE="`TypeOf "$1"`" || { Warn "Undefined variable $1"; return 1; }
	case "$TYPE" in
		"i") echo "1";; # always 1 for numbers
		"-") eval echo "\${#$1}";; # length for strings
		"a") eval echo "\${#$1[*]}";; # elements count for arrays
	esac
}
function IsEmpty () {
	(( $# < 1 )) && Usage "<variable>"
	IsSet "$1" || { Warn "Undefined variable $1"; return 0; }
	return "`Length "$1"`"
}

function CheckInteger () {
	[[ "$1" =~ $RE_INT ]]
}

function Get () { #`FunctionCallProcess "VAR" "$@"`
	(( $# < 1 )) && Usage "<var> [<var> ...]"
	Integer COUNT=0 KEY=-1
	String ARG TYPE VAR JUNK RESULT=""
	for ARG; do
		read VAR TYPE <<< "${ARG/:/ }"
		read VAR JUNK <<< "${VAR/[/ }"
		[[ "$JUNK" ]] && KEY="${JUNK%]}"


echo "$VAR-$TYPE-$KEY-$JUNK"


		if not JUNK=`declare -p "$VAR" 2> /dev/null`; then
			Debug "Undefined variable: $VAR"
			continue
		fi
		read ARG ARG JUNK <<< "$JUNK"

		if [[ "$ARG" == "-a" ]]; then
			if (( $KEY >= 0 )); then

#			(( "$KEY" )) || KEY=0



				VAR="$VAR[$KEY]"

echo "$VAR"

				if not [[ "${!VAR:+test}" ]]; then
					Debug "Undefined array element: $VAR"
					continue
				fi
			else
				VAR="$VAR[@]"
			fi
		fi






		case "$TYPE" in
			"i"|"integer")

:

			;;
			""|"a"|"array"|"s"|"string")
				: # no typecasting
			;;
			*)
				Debug "Unknown type: $TYPE, assuming no typecasting"
			;;
		esac



		RESULT+=`ShellEscape ${!VAR}`
		if (( $COUNT < $# )); then
			RESULT+=' '
			COUNT+=1
		fi
	done



#	String   V=""   KEYS="`ArrayKeys "$VAR"`"   FOUND=""
#	[[ "$KEYS" ]] || {  }
#	if [[ "0" != "$KEYS" ]]; then # VAR contains an array
#		for V in @ $KEYS; do
#			[[ "$V" = "$KEY" ]] && FOUND="YES"
#		done
#		if not [[ "$FOUND" ]]; then
#			Debug "Undefined array element: $VAR[$KEY]"
#			return 0
#		fi
#		VAR="$VAR[$KEY]"
#	fi
#	for V in "${!VAR}"; do ShellEscape "$V"; done
}

#
# STRING FUNCTIONS
#
function ShellEscape () {
	(( $# < 1 )) && Usage "<string> [<string> ...]"
	Integer COUNT=0
	String ARG TMP RESULT=""
	for ARG in "$@"; do
		if [[ "$ARG" ]]; then
			printf -v TMP '%q' "$ARG"
			RESULT+="$TMP"
		else
			RESULT+="''"
		fi
		if (( $COUNT < $# )); then
			RESULT+=' '
			COUNT+=1
		fi
	done
	echo "$RESULT"
}
#
# ARRAY FUNCTIONS
#
function ArrayKeys () {
	(( $# < 1 )) && Usage "<array>"
	eval echo "\${!$1[*]}" # just echo is enough, as all the keys are numbers
}
function ArrayImplode () {
	# FIXME: add formatting options
	Array   GNUOPT_escape   GNUOPT_noempty
	GNUOptionsParse "$@"
	eval set -- "$REPLY"
	(( $# < 1 )) && Usage "[options] <array> [<glue>]"
	String   V=""   IMPLODED=""   PTR="$1"   P2="$1[@]"   GLUE="${2:- }"
	(( "`Length "$PTR"`" )) || return 0 # empty array
	for V in "${!P2}"; do
		if not IsEmpty GNUOPT_noempty && IsEmpty V; then continue; fi # skip an empty element if we have to
		IsEmpty GNUOPT_escape || V="`ShellEscape "$V"`"
		IMPLODED+="${V}${GLUE}"
	done
	echo "${IMPLODED: 0 : $(( "`Length IMPLODED`" - "`Length GLUE`" )) }" # trim the last portion of glue
}
function ArrayValues () {
	# use it like this:
	# eval NEW=( `ArrayValues "$OLD"` )
	(( $# < 1 )) && Usage "<array>"
	ArrayImplode --escape "$1"
}
function ArrayFirstKey () {
	(( $# < 1 )) && Usage "<array>"
	Array   KEYS=( `ArrayKeys "$1"` )
	(( ${#KEYS[*]} < 1 )) && return 0 # empty array
	echo ${KEYS[0]}
}
function ArrayLastKey () {
	(( $# < 1 )) && Usage "<array>"
	Array   KEYS=( `ArrayKeys "$1"` )
	(( ${#KEYS[*]} < 1 )) && return 0 # empty array
	echo ${KEYS[ $(( "`Length KEYS`" - 1 )) ]}
}
function ArrayPush () {
	(( $# < 2 )) && Usage "<array> <value> [<value> ...]"
	String   PTR="$1"
	IsArray $PTR || { Warn "Undefined array $PTR"; return 1; }
	shift
	eval "$PTR+=( \"\$@\" )"
}
function ArrayPop () {
	# the extracted element will be in $REPLY
	(( $# < 1 )) && Usage "<array>"
	String   KEY="`ArrayLastKey $1`"
	[ "$KEY" ] || return 0 # empty array
	String   PTR="$1[$KEY]"
	REPLY="${!PTR}"
	unset "$PTR"
}
function ArrayShift () {
	# the extracted element will be in $REPLY
	(( $# < 1 )) && Usage "<array>"
	String   KEY="`ArrayFirstKey "$1"`"
	[ "$KEY" ] || return 0 # empty array
	String   PTR="$1[$KEY]"
	REPLY="${!PTR}"
	eval "$1=( \"\${$1[@]:1}\" )"
}
function ArrayUnshift () {
	(( $# < 2 )) && Usage "<array> <value> [<value> ...]"
	String   PTR="$1"   P2="$1[@]"
	IsArray $PTR || { Warn "Undefined array $PTR"; return 1; }
	shift
	eval "$PTR=( \"\$@\" \"\${!P2}\" )"
}
function ArrayReverse () {
	(( $# < 1 )) && Usage "<array>"
	String   PTR=""   KEY=""
	Array   KEYS=( `ArrayKeys "$1"` )   REVERSED=()
	for (( KEY=${#KEYS[@]} - 1; $KEY + 1; KEY--)); do
		PTR="$1[$KEY]"
		REVERSED+=( "${!PTR}" )
	done
	ArrayValues REVERSED
}
#
# MISCELLANEOUS FUNCTIONS
#
function LibclandestineVersionGE () {
	(( $# < 1 )) && Usage "<major> [<minor> <revision> ...]"
	String   V=""
	Array   VER=( "${LIBCLANDESTINE[@]}" )
	for V; do
		ArrayShift VER
		[ "$REPLY" ] || REPLY="0"
		[ "$REPLY" -lt "$V" ] && return 1
		[ "$REPLY" -gt "$V" ] && return 0
	done
	return 0
}
function GNUOptionsParse () {
	# you use it like this:
	#   Array   GNUOPT_opt1   GNUOPT_opt2=( default )   GNUOPT_opt3 ...
	#   GNUOptionsParse "$@"
	#   eval set -- "$REPLY"
	# now you have all the options stripped from your positional parameters
	# and stuffed into the corresponding variables (see docs for more info)
	REPLY=""
	String   OPTION=""   VALUE=""   PARSE="YES"
	for OPTION in "$@"; do
		[[ "$PARSE" && "--" = "$OPTION" ]] && { PARSE=""; continue; }
		if [[ "$PARSE" && "--" = "${OPTION:0:2}" ]]; then
			OPTION="${OPTION:2}"
			read OPTION VALUE <<<"${OPTION/=/ }"
			if IsSet "GNUOPT_$OPTION"; then
				ArrayPush "GNUOPT_$OPTION" "$VALUE"
			else
				Debug "Unknown option: $OPTION"
			fi
			continue
		fi
		REPLY+="`ShellEscape "$OPTION"` "
	done
}
#
# AUTORUN
#
declare -a GNUOPT_debug GNUOPT_help GNUOPT_version
GNUOptionsParse "$@"
eval set -- "$REPLY"

if not IsEmpty GNUOPT_help; then
	SOURCE=( "${BASH_SOURCE[@]}" )
	for FNAME in `ArrayReverse SOURCE`; do
		OLDIFS="$IFS"
		IFS=$'\n'
		for LINE in `grep -e '^##' $FNAME`; do
			eval echo \"${LINE//###/}\"
		done
		IFS=$OLDIFS
		echo ""
	done
	exit 0
fi
if not IsEmpty GNUOPT_version; then
	SOURCE=( "${BASH_SOURCE[@]}" )
	SED='/^#\$/ s|%\([^%]*\)%|\${\1[0]}.\${\1[1]}.\${\1[2]} (\${\1[3]})|gp'
	for FNAME in `ArrayReverse SOURCE`; do
		OLDIFS="$IFS"
		IFS=$'\n'
		for LINE in `sed --quiet "$SED" "$FNAME"`; do
			eval echo \"${LINE//##$/}\"
		done
		IFS="$OLDIFS"
	done
	exit 0
fi
unset GNUOPT_help GNUOPT_version
